/*
*
==============================================================================
*  Name        : 
*  Part of     : Omen Game Engine
*  Description : 
*  Version     : 1.0
*
*  Copyright (c) 2013 Lauri Kortevaara.
* 
==============================================================================
*/

#define OMEN_SOURCE 1

/// Omen Headers
#include "OmenKernel.h"
#include "OmenConfig.h"

#include "OmenKeyboardEvent.h"
#include "OmenMouseEvent.h"
#include <OmenFrameEvent.h>

#include "OmenModelFileReader.h"
#include "OmenSceneManager.h"
#include "OmenAssetManager.h"
#include "OmenTimer.h"

/// STL Headers
#include <iostream>
#include <algorithm>
#include <map>
#include <string>
#include <memory>

using namespace Omen;

/// The singleton pattern instance of Omen Kernel
std::unique_ptr<Omen::Kernel> Omen::Kernel::_instance = NULL;

// Static utility functions
static void replaceEnvVariables( std::wstring& str );

///
/// <summary>Private constructor of Omen Kernel. Use Omen::Kernel::createKernel() to get an instance of the kernel</summary>
///
Kernel::Kernel() :
  Omen::PropertyObject( L"Omen::Kernel" ),
  m_graphicsServer(NULL),
  m_inputServer(NULL),
  m_storageServer(NULL),
  m_sceneManager(NULL),
  m_assetManager(NULL),
  m_pTimer(NULL)
{
}

///
/// <summary>Returns the Storage server</summary>
///
IKernel& Kernel::getKernel()
{
    if( !Kernel::_instance )
    {
        Kernel::_instance = std::unique_ptr<Omen::Kernel>(new Kernel);
    }
    
    return *Kernel::_instance;
}

Omen::Property& Kernel::getProperty( std::wstring strPropertyName )
{
    return Omen::PropertyObject::property(strPropertyName);
}

void  Kernel::setProperty( std::wstring strPropertyName, const std::wstring& strProperty)
{
    Omen::PropertyObject::setProperty( strPropertyName, strProperty );
}

///
/// <summary>Initialize the Engine using given config file. By default it's config.cfg</summary>
///
bool Kernel::initialize( const std::wstring& configFilePath )
{
    bool bInitOk = false;
    std::wcout << L"Omen::Kernel: Starting initialization..." << std::endl;

    try {
        // Initialize keyboard button states
        Omen::KeyboardEvent::initialize();

        // Set Default configuration properties
        std::wstring pluginPath = std::wstring(OMEN_DEFAULT_PLUGIN_PATH);
        replaceEnvVariables(pluginPath);
        setProperty( L"plugin-path", pluginPath );

        std::wcout << L"Omen::Kernel: Reading configuration file:" << configFilePath << std::endl;

        Omen::XmlReader configReader( configFilePath );
        if( configReader.parseDocument() )
        {
            Omen::XmlDocument config;
            if( configReader.getDocument(config) )
            {
                for( auto rootNode = config.begin(); rootNode!= config.end(); ++rootNode )
                {
                    XmlNode node = *rootNode;
                    parseConfigNode(node);
                }
            
                bInitOk = true;
            }
        }
        else
        {
            std::wcout << L"Omen::Kernel: Unable to read configuration file :" << configFilePath << std::endl;
        }
    } catch( std::string& e )
    {
#ifdef _WIN32
        MessageBoxA( NULL, e.c_str(), "Omen::Kernel::Initialize ERROR!", MB_OK );
#endif
    }

    if( bInitOk )
        std::wcout << L"Omen::Kernel: Initialization ended successfully!" << std::endl;
    else
        std::wcout << L"Omen::Kernel: Initialization ended in failure!" << std::endl;

    return bInitOk;
}

///
/// <summary>static replaceEnvVariables will replace environment variables in given string with their existing values</summary>
///
static void replaceEnvVariables( std::wstring& str )
{
    while( str.find( L"$(" ) != std::wstring::npos )
    {
        std::size_t envStart = str.find( L"$(" );
        std::size_t envEnd = str.find( L")", envStart );
        
        std::wstring envVariable = str.substr( envStart+2, envEnd-envStart-2 );

#ifdef _WIN32
        wchar_t envVar[512];
        DWORD len = GetEnvironmentVariable( (wchar_t*)envVariable.c_str(), envVar, 512 );
        if( !len )
            wcscpy_s(envVar, sizeof(envVar), L"\0" );

        std::wstring wcsVar = envVar;
        str.replace( envStart, envEnd-envStart+1, wcsVar );
#endif
    }
}

/// <summary>Adds a new propertyobject to the kernel. Returns true if succeeded, false if there is already a propertyobject with name name</summary>
bool Kernel::addPropertyObject( Omen::PropertyObject& propertyObject )
{
    bool bAlreadyHasPOWithSameName = std::find( m_propertyObjects.begin(), m_propertyObjects.end(), propertyObject ) != m_propertyObjects.end();
    if( !bAlreadyHasPOWithSameName )
        m_propertyObjects.push_back( propertyObject );
    return !bAlreadyHasPOWithSameName;
}

/// <summary>Returns true if the propertyObject has the object with given name, false otherwise</summary>
bool Kernel::getPropertyObject( const std::wstring& strPropertyObjectName, Omen::PropertyObject& propertyObject )
{
    bool bPropertyObjectFound = std::find( m_propertyObjects.begin(), m_propertyObjects.end(), strPropertyObjectName ) != m_propertyObjects.end();
    if( bPropertyObjectFound )
        propertyObject = *std::find( m_propertyObjects.begin(), m_propertyObjects.end(), strPropertyObjectName );
    return bPropertyObjectFound;
}

/// <summary>Parse the config nodes</summary>
void Kernel::parseConfigNode( Omen::XmlNode& node )
{
    std::wstring nodeName = node.name();
    std::wstring nodeData = node.data();

    std::vector<Omen::XmlNode>& children = node.children();
    std::vector<Omen::XmlNodeAttribute>& attributes = node.attributes();

    if( nodeName == L"property" )
    {
        std::wstring propertyName;
        std::wstring propertyValue;

        // Parse Attributes
        for( auto attrib= attributes.begin(); attrib!= attributes.end(); ++attrib )
        {
            Omen::XmlNodeAttribute& attribute = *attrib;
            std::wstring& attribName = attribute.name();
            std::wstring& attribValue = attribute.value();
            if( attribName.compare( L"name" ) == 0 )
                propertyName = attribValue;
            else
            if( attribName.compare( L"value" ) == 0 )
                propertyValue =  attribValue;
        }
        
        std::wstring configuration = L"$(Configuration)";
        replaceEnvVariables(configuration);

        // Add new kernel property
        replaceEnvVariables( propertyValue );
        this->setProperty( propertyName, propertyValue );
    }
    else
    if( nodeName == L"property-object" )
    {
        std::map< std::wstring, std::wstring > props;

        // Parse Attributes
        for( auto attrib= attributes.begin(); attrib!= attributes.end(); ++attrib )
        {
            Omen::XmlNodeAttribute& attribute = *attrib;
            std::wstring& attribName = attribute.name();
            std::wstring& attribValue = attribute.value();
            // Add new kernel property
            replaceEnvVariables( attribValue );
            props[attribName] = attribValue;
        }
        
        std::wstring propObjName = props[L"name"];
        if( !propObjName.empty() )
        {
            PropertyObject propObj(propObjName);
            for( auto iter = props.begin(); iter != props.end(); ++iter )
                if(iter->first != L"name")
                    propObj.setProperty( iter->first, iter->second );
            m_propertyObjects.push_back(propObj);
        }
    }
    else
    {
        if( !nodeData.empty() )
        {
            replaceEnvVariables(nodeData);
            setProperty( nodeName, nodeData );
        }
    }

    // Parse child nodes
    for( auto child= children.begin(); child!= children.end(); ++child )
    {
        Omen::XmlNode& childNode = *child;
        parseConfigNode( childNode );
    }
}

///
/// <summary>Returns the Storage server</summary>
///
 IStorageServer* Kernel::getStorageServer()
{
    if( !m_storageServer )
    {
        m_storageServer = new Omen::StorageServer;
        m_storageServer->addModelFileReader( static_cast<Omen::IModelFileReader*>(new Omen::ModelFileReader()) );
    }

    return m_storageServer;
}

///
/// <summary>Returns the Graphics server</summary>
///
IGraphicsServer*  Kernel::getGraphicsServer() 
{
    if( !m_graphicsServer )
        m_graphicsServer = new Omen::GraphicsServer;

    return m_graphicsServer;
}

///
/// <summary>Returns the Input server</summary>
///
 IInputServer* Kernel::getInputServer()
{
    if( !m_inputServer )
        m_inputServer = new Omen::InputServer;

    return m_inputServer;
}


///
/// <summary>Loads a plugin</summary>
/// <param name="filename">File the plugin will be loaded from</param>
///
void Kernel::loadPlugin(const std::wstring &filename) 
{
    std::wcout << L"Omen::Kernel: Loading plugin " << filename << std::endl;

    if(m_loadedPlugins.find(filename) == m_loadedPlugins.end()) 
    {
        // Load the plugin
        try {
            Plugin plugin(filename);

            // Register the pugin in kernel
            plugin.registerPlugin(this);

            // Insert the loaded plugin in map
            m_loadedPlugins.insert(PluginMap::value_type(filename, Plugin(filename)));
            std::wcout << L"Omen::Kernel: Plugin loaded successfully!" << std::endl;
        }
        catch(std::exception &e) {
            std::wcout << e.what();
        }
    }
}

///
/// <summary>Loads a plugin</summary>
/// <param name="filename">File the plugin will be loaded from</param>
///
void Kernel::loadPlugins(const std::wstring &pluginPath) 
{
    WIN32_FIND_DATA findData;


    std::wstring path;
    path.append( pluginPath );
    if( pluginPath.back() != L'/' && pluginPath.back() != L'\\' )
        path.append( L"/" );
    path.append( L"*.dll" );

    HANDLE hFind = FindFirstFile( path.c_str(), &findData );
    while( hFind != INVALID_HANDLE_VALUE )
    {
        std::wstring fileName = findData.cFileName;
        if( !fileName.empty() )
        {
            if( pluginPath.back() != L'/' && pluginPath.back() != L'\\' )
                fileName.insert(0, L"/" );
            fileName.insert( 0, pluginPath );
            loadPlugin(fileName);
        }
        if( !FindNextFile( hFind, &findData ) )
            break;
    }
}

///
/// <summary>Shut down the application</summary>
///
void Kernel::shutDown()
{
    if( timer() )
        timer()->stop();

#ifdef _WIN32
    PostMessage( 0, WM_QUIT, 0, 0 );
#else
#endif

}

///
/// <summary>Kernel::run starts running the kernel's event loop</summary>
///
void Kernel::run()
{
    if( timer() )
        timer()->start();

    MSG msg;
    bool done, result;

    // Initialize the message structure.
    ZeroMemory(&msg, sizeof(MSG));
    
    // Loop until there is a quit message from the window or the user.
    done = false;
    while(!done)
    {
        if( this->m_sceneManager->currentRenderer() )
        if( GetForegroundWindow() == (HWND)this->m_sceneManager->currentRenderer()->windowHandle() )
        {
            result = frame();
            if(!result)
            {
                done = true;
            }
        }

        // Handle the windows messages.
        while(PeekMessage(&msg, 0, 0, 0, PM_REMOVE ))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        
            switch( msg.message )
            {
                // If windows signals to end the application then exit out.
                case WM_QUIT:
                {
                    done = true;
                    break;
                }
                case WM_KEYDOWN:
                {
                    //std::wcout << L"Kernel KEYDOWN" << std::endl;
                    sendEvent( new KeyboardEvent( msg.wParam ) );
                    break;
                }
                case WM_KEYUP:
                {
                    //std::wcout << L"Kernel KEYUP" << std::endl;
                    sendEvent( new KeyboardEvent( -(long)msg.wParam ) );
                    break;
                }
            }
        }
    }

    return;
}

///
/// <summary>Kernel:frame handles the process of handling inputs, physics, LOD and rendering</summary>
///
bool Kernel::frame()
{
    static Omen::Float fps_time = timer()->totalTime();
    static unsigned long frameCounter = 0;
    
    sendEvent(new Omen::FrameEvent());

    if( m_inputServer )
        m_inputServer->handleInput();

    Omen::IScene* pCurrentScene = m_sceneManager->currentScene();

    if( pCurrentScene && m_sceneManager->currentRenderer() )
    {
        //pCurrentScene->checkCollisions();
        pCurrentScene->render(m_sceneManager->currentRenderer());
    }

    frameCounter++;
    
    std::wstringstream wss;
    std::wstring strFPS;
    wss = std::wstringstream(strFPS);

    Omen::Float cur_time = timer()->totalTime();
    wss << L"FPS: " << 1.0/(cur_time-fps_time);
    //std::wcout << wss.str() << std::endl; //

    fps_time = timer()->totalTime();
    timer()->elapsedTime(true); // Initialize the frame timer (deltaTime)
    return true;
}

///
/// <summary>Returns the kernel Windows proceduce pointer</summary>
///
WinMessageHandler Kernel::GetWinProc()
{
    return MessageHandler;
}

///
/// <summary>Kernel Windows message handler</summary>
///
int Kernel::MessageHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    int ret = 0;

    switch(msg)
    {
        /*case WM_KEYDOWN:
        {
            if( Kernel::_instance )
            {
                Kernel::_instance->sendEvent( new Omen::KeyboardEvent(lparam) );
            }
            ret = 0;
            break;
        }*/
        case WM_MOUSEMOVE:
        {
            if( Kernel::_instance )
            {
                Omen::MouseEvent* pMouseEvent = new Omen::MouseEvent();
                pMouseEvent->setMousePosition( Omen::MouseEvent::MousePosition(LOWORD(lparam), HIWORD(lparam) ));
                Kernel::_instance->sendEvent( pMouseEvent );
            }
            return 0;
        }
        case WM_MOUSEWHEEL:
        {
            if( Kernel::_instance )
            {

            }
            return 0;
        }
        // Check if a key has been released on the keyboard.
        /*case WM_KEYUP:
        {
            // If a key is released then send it to the input object so it can unset the state for that key.
            //m_Input->KeyUp((unsigned int)wparam);
            ret = 0;
            break;
        }*/
    
        // Any other messages send to the default message handler as our application won't make use of them.
        default:
        {
        
            return DefWindowProc(hwnd, msg, wparam, lparam);
        }
    }

    return ret;
}

///
/// <summary>Registers a new event handler</summary>
///
void Kernel::registerEventHandler( Omen::IEventHandler* handler )
{
    m_eventHandlers.push_back(handler);
}

///
/// <summary>Sends a new kernel event</summary>
///
 void Kernel::sendEvent( const Omen::Event* event )
{
    for( auto iter = m_eventHandlers.begin(); iter != m_eventHandlers.end(); ++iter )
    {
        Omen::IEventHandler* pHandler = *iter;
        pHandler->handleEvent(event);
    }
    delete event;
}


///
/// <summary>Returns the scene manager</summary>
///
Omen::ISceneManager* Kernel::getSceneManager()
{
    if( !m_sceneManager )
        m_sceneManager = new Omen::SceneManager(this);
    
    return m_sceneManager;
}

///
/// <summary>Returns the scene manager</summary>
///
Omen::IAssetManager* Kernel::getAssetManager()
{
    if( !m_assetManager )
        m_assetManager = new Omen::AssetManager(this);
    
    return m_assetManager;
}

///
/// <summary>Returns the kernel timer object</summary>
///
Omen::ITimer* Kernel::timer()
{
    if( !m_pTimer )
        m_pTimer = new Omen::Timer;
    
    return m_pTimer;
}